mod discard_tracked;
mod stage_tracked;

pub use discard_tracked::discard_lines;
pub use stage_tracked::stage_lines;

use super::{
    diff::DiffLinePosition, patches::HunkLines, utils::work_dir,
};
use crate::error::Result;
use git2::{DiffLine, Repository};
use std::{
    collections::HashSet, convert::TryFrom, fs::File, io::Read,
};

const NEWLINE: char = '\n';

#[derive(Default)]
struct NewFromOldContent {
    lines: Vec<String>,
    old_index: usize,
}

impl NewFromOldContent {
    fn add_from_hunk(&mut self, line: &DiffLine) -> Result<()> {
        let line = String::from_utf8(line.content().into())?;

        let line = if line.ends_with(NEWLINE) {
            line[0..line.len() - 1].to_string()
        } else {
            line
        };

        self.lines.push(line);

        Ok(())
    }

    fn skip_old_line(&mut self) {
        self.old_index += 1;
    }

    fn add_old_line(&mut self, old_lines: &[&str]) {
        self.lines.push(old_lines[self.old_index].to_string());
        self.old_index += 1;
    }

    fn catchup_to_hunkstart(
        &mut self,
        hunk_start: usize,
        old_lines: &[&str],
    ) {
        while hunk_start > self.old_index + 1 {
            self.add_old_line(old_lines);
        }
    }

    fn finish(mut self, old_lines: &[&str]) -> String {
        for line in old_lines.iter().skip(self.old_index) {
            self.lines.push(line.to_string());
        }
        let lines = self.lines.join("\n");
        if lines.ends_with(NEWLINE) {
            lines
        } else {
            let mut lines = lines;
            lines.push(NEWLINE);
            lines
        }
    }
}

// this is the heart of the per line discard,stage,unstage. heavily inspired by the great work in nodegit: https://github.com/nodegit/nodegit
pub(crate) fn apply_selection(
    lines: &[DiffLinePosition],
    hunks: &[HunkLines],
    old_lines: Vec<&str>,
    is_staged: bool,
    reverse: bool,
) -> Result<String> {
    let mut new_content = NewFromOldContent::default();
    let lines = lines.iter().collect::<HashSet<_>>();

    let char_added = if reverse { '-' } else { '+' };
    let char_deleted = if reverse { '+' } else { '-' };

    let mut first_hunk_encountered = false;
    for hunk in hunks {
        let hunk_start = if is_staged || reverse {
            usize::try_from(hunk.hunk.new_start)?
        } else {
            usize::try_from(hunk.hunk.old_start)?
        };

        if !first_hunk_encountered {
            let any_slection_in_hunk =
                hunk.lines.iter().any(|line| {
                    let line: DiffLinePosition = line.into();
                    lines.contains(&line)
                });

            first_hunk_encountered = any_slection_in_hunk;
        }

        if first_hunk_encountered {
            new_content.catchup_to_hunkstart(hunk_start, &old_lines);

            for hunk_line in &hunk.lines {
                let hunk_line_pos: DiffLinePosition =
                    hunk_line.into();
                let selected_line = lines.contains(&hunk_line_pos);

                log::debug!(
                    // println!(
                    "{} line: {} [{:?} old, {:?} new] -> {}",
                    if selected_line { "*" } else { " " },
                    hunk_line.origin(),
                    hunk_line.old_lineno(),
                    hunk_line.new_lineno(),
                    String::from_utf8_lossy(hunk_line.content())
                        .trim()
                );

                if hunk_line.origin() == '<' {
                    break;
                }

                if (is_staged && !selected_line)
                    || (!is_staged && selected_line)
                {
                    if hunk_line.origin() == char_added {
                        new_content.add_from_hunk(hunk_line)?;
                        if is_staged {
                            new_content.skip_old_line();
                        }
                    } else if hunk_line.origin() == char_deleted {
                        if !is_staged {
                            new_content.skip_old_line();
                        }
                    } else {
                        new_content.add_old_line(&old_lines);
                    }
                } else {
                    if hunk_line.origin() != char_added {
                        new_content.add_from_hunk(hunk_line)?;
                    }

                    if (is_staged
                        && hunk_line.origin() != char_deleted)
                        || (!is_staged
                            && hunk_line.origin() != char_added)
                    {
                        new_content.skip_old_line();
                    }
                }
            }
        }
    }

    Ok(new_content.finish(&old_lines))
}

pub(crate) fn load_file(
    repo: &Repository,
    file_path: &str,
) -> Result<String> {
    let repo_path = work_dir(repo)?;
    let mut file = File::open(repo_path.join(file_path).as_path())?;
    let mut res = String::new();
    file.read_to_string(&mut res)?;

    Ok(res)
}
